﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core
{
	/// <summary>Extends a <see cref="TypeBuilder"/> with higher-level methods.</summary>
	public class TypeBuilderExtender
	{
		//	Method attributes for methods hidden by an interface implementation.
		private const MethodAttributes HIDDEN_METHOD_ATTRIBUTES =
			MethodAttributes.Private
			| MethodAttributes.HideBySig
			| MethodAttributes.NewSlot
			| MethodAttributes.Virtual
			| MethodAttributes.Final;
		//	Method attributes for non-hidden methods of an interface implementation.
		private const MethodAttributes PUBLIC_METHOD_ATTRIBUTES =
			MethodAttributes.Public
			| MethodAttributes.HideBySig
			| MethodAttributes.NewSlot
			| MethodAttributes.Virtual
			| MethodAttributes.Final;
		//	Method attributes for get & set methods hidden by an interface implementation.
		private const MethodAttributes HIDDEN_METHOD_GET_SET_ATTRIBUTES =
			HIDDEN_METHOD_ATTRIBUTES
			| MethodAttributes.SpecialName;
		//	Method attributes for get & set methods non-hidden by an interface implementation.
		private const MethodAttributes PUBLIC_METHOD_GET_SET_ATTRIBUTES =
			PUBLIC_METHOD_ATTRIBUTES
			| MethodAttributes.SpecialName;
		//	Method attributes for add & remove methods hidden by an interface implementation.
		private const MethodAttributes HIDDEN_METHOD_ADD_REMOVE_ATTRIBUTES =
			HIDDEN_METHOD_ATTRIBUTES
			| MethodAttributes.SpecialName;
		//	Method attributes for add & remove methods non-hidden by an interface implementation.
		private const MethodAttributes PUBLIC_METHOD_ADD_REMOVE_ATTRIBUTES =
			PUBLIC_METHOD_ATTRIBUTES
			| MethodAttributes.SpecialName;

		private readonly TypeBuilder _typeBuilder;
		private readonly IDictionary<string, FieldBuilder> _fieldDictionary = new Dictionary<string, FieldBuilder>();

		/// <summary>Construct an extender by wrapping a <see cref="TypeBuilder"/>.</summary>
		/// <param name="typeBuilder"></param>
		public TypeBuilderExtender(TypeBuilder typeBuilder)
		{
			_typeBuilder = typeBuilder;
		}

		/// <summary>Exposes the underlying <see cref="TypeBuilder"/>.</summary>
		public TypeBuilder TypeBuilder
		{
			get { return _typeBuilder; }
		}

		/// <summary>Implementing a method from an interface.</summary>
		/// <param name="methodInfo">Method info on an interface.</param>
		/// <param name="isPrivatelyImplemented">
		/// If <c>true</c>, defines the method as <c>hidden</c> by the interface.  Otherwise, defines it publically.
		/// </param>
		/// <returns></returns>
		public ILGeneratorExtender ImplementInterfaceMethod(MethodInfo methodInfo, bool isPrivatelyImplemented)
		{
			if (methodInfo == null)
			{
				throw new ArgumentNullException("methodInfo");
			}

			Type interfaceType = methodInfo.DeclaringType;

			if (!interfaceType.IsInterface)
			{
				throw new ArgumentException("Should belong to an interface, not a concrete type", "methodInfo");
			}

			Type[] parameterTypeList = Array.ConvertAll(
				methodInfo.GetParameters(),
				delegate(ParameterInfo parameter) { return parameter.ParameterType; });
			string methodName = isPrivatelyImplemented
				? string.Concat(interfaceType.FullName, ".", methodInfo.Name)
				: methodInfo.Name;
			MethodAttributes methodAttributes = isPrivatelyImplemented ? HIDDEN_METHOD_ATTRIBUTES : PUBLIC_METHOD_ATTRIBUTES;
			MethodBuilder methodBuilder = TypeBuilder.DefineMethod(
				methodName,
				methodAttributes,
				methodInfo.ReturnType,
				parameterTypeList);

			TypeBuilder.DefineMethodOverride(methodBuilder, methodInfo);

			return new ILGeneratorExtender(methodBuilder.GetILGenerator(), _fieldDictionary);
		}

		/// <summary>Implementing a property from an interface.</summary>
		/// <param name="propertyInfo"></param>
		/// <param name="isPrivatelyImplemented">
		/// If <c>true</c>, defines the method as <c>hidden</c> by the interface.  Otherwise, defines it publically.
		/// </param>
		/// <param name="ilGeneratorGet"></param>
		/// <param name="ilGeneratorSet"></param>
		public void ImplementProperty(
			PropertyInfo propertyInfo,
			bool isPrivatelyImplemented,
			out ILGeneratorExtender ilGeneratorGet,
			out ILGeneratorExtender ilGeneratorSet)
		{
			if (propertyInfo == null)
			{
				throw new ArgumentNullException("propertyInfo");
			}

			Type interfaceType = propertyInfo.DeclaringType;

			if (!interfaceType.IsInterface)
			{
				throw new ArgumentException("Should belong to an interface, not a concrete type", "propertyInfo");
			}

			string propertyName = isPrivatelyImplemented
				? string.Concat(interfaceType.FullName, ".", propertyInfo.Name)
				: propertyInfo.Name;
			PropertyBuilder propertyBuilder = TypeBuilder.DefineProperty(
				propertyName,
				PropertyAttributes.HasDefault,
				propertyInfo.PropertyType,
				null);
			MethodAttributes methodAttributes = isPrivatelyImplemented
				? HIDDEN_METHOD_GET_SET_ATTRIBUTES
				: PUBLIC_METHOD_GET_SET_ATTRIBUTES;

			if (propertyInfo.CanRead)
			{
				MethodInfo getMethodInfo = propertyInfo.GetGetMethod();
				string methodName = isPrivatelyImplemented
					? string.Concat(interfaceType.FullName, ".", getMethodInfo.Name)
					: getMethodInfo.Name;
				MethodBuilder methodBuilder = TypeBuilder.DefineMethod(
					methodName,
					methodAttributes,
					getMethodInfo.ReturnType,
					Type.EmptyTypes);

				ilGeneratorGet =
					new ILGeneratorExtender(methodBuilder.GetILGenerator(), _fieldDictionary);
				propertyBuilder.SetGetMethod(methodBuilder);
				TypeBuilder.DefineMethodOverride(methodBuilder, getMethodInfo);
			}
			else
			{
				ilGeneratorGet = null;
			}
			if (propertyInfo.CanWrite)
			{
				MethodInfo setMethodInfo = propertyInfo.GetSetMethod();
				string methodName = isPrivatelyImplemented
					? string.Concat(interfaceType.FullName, ".", setMethodInfo.Name)
					: setMethodInfo.Name;
				MethodBuilder methodBuilder = TypeBuilder.DefineMethod(
					methodName,
					methodAttributes,
					setMethodInfo.ReturnType,
					new Type[] { propertyInfo.PropertyType });

				ilGeneratorSet = new ILGeneratorExtender(methodBuilder.GetILGenerator(), _fieldDictionary);
				propertyBuilder.SetSetMethod(methodBuilder);
				TypeBuilder.DefineMethodOverride(methodBuilder, setMethodInfo);
			}
			else
			{
				ilGeneratorSet = null;
			}
		}

		/// <summary>Implementing an event from an interface.</summary>
		/// <param name="eventInfo"></param>
		/// <param name="isPrivatelyImplemented">
		/// If <c>true</c>, defines the method as <c>hidden</c> by the interface.  Otherwise, defines it publically.
		/// </param>
		/// <param name="ilGeneratorAdd"></param>
		/// <param name="ilGeneratorRemove"></param>
		public void ImplementEvent(
			EventInfo eventInfo,
			bool isPrivatelyImplemented,
			out ILGeneratorExtender ilGeneratorAdd,
			out ILGeneratorExtender ilGeneratorRemove)
		{
			if (eventInfo == null)
			{
				throw new ArgumentNullException("eventInfo");
			}

			Type interfaceType = eventInfo.DeclaringType;

			if (!interfaceType.IsInterface)
			{
				throw new ArgumentException("Should belong to an interface, not a concrete type", "eventInfo");
			}

			MethodAttributes methodAttributes = isPrivatelyImplemented
				? HIDDEN_METHOD_ADD_REMOVE_ATTRIBUTES
				: PUBLIC_METHOD_ADD_REMOVE_ATTRIBUTES;
			string eventName = isPrivatelyImplemented
				? string.Concat(interfaceType.FullName, ".", eventInfo.Name)
				: eventInfo.Name;
			EventBuilder eventBuilder = TypeBuilder.DefineEvent(eventName, eventInfo.Attributes, eventInfo.EventHandlerType);
			MethodInfo addMethodInfo = eventInfo.GetAddMethod();
			string addMethodName = isPrivatelyImplemented
				? string.Concat(interfaceType.FullName, ".", addMethodInfo.Name)
				: addMethodInfo.Name;
			MethodInfo removeMethodInfo = eventInfo.GetRemoveMethod();
			string removeMethodName = isPrivatelyImplemented
				? string.Concat(interfaceType.FullName, ".", removeMethodInfo.Name)
				: removeMethodInfo.Name;
			MethodBuilder addMethodBuilder = TypeBuilder.DefineMethod(
				addMethodName,
				methodAttributes,
				null,
				new Type[] { eventInfo.EventHandlerType });
			MethodBuilder removeMethodBuilder = TypeBuilder.DefineMethod(
				removeMethodName,
				methodAttributes,
				null,
				new Type[] { eventInfo.EventHandlerType });

			ilGeneratorAdd =
				new ILGeneratorExtender(addMethodBuilder.GetILGenerator(), _fieldDictionary);
			ilGeneratorRemove =
				new ILGeneratorExtender(removeMethodBuilder.GetILGenerator(), _fieldDictionary);
			eventBuilder.SetAddOnMethod(addMethodBuilder);
			eventBuilder.SetRemoveOnMethod(removeMethodBuilder);
			TypeBuilder.DefineMethodOverride(addMethodBuilder, addMethodInfo);
			TypeBuilder.DefineMethodOverride(removeMethodBuilder, removeMethodInfo);
		}

		/// <summary>Defines a static constructor.</summary>
		/// <returns></returns>
		public ILGeneratorExtender DefineStaticConstructor()
		{
			MethodAttributes attributes = MethodAttributes.HideBySig
				| MethodAttributes.Private
				| MethodAttributes.Static
				| MethodAttributes.SpecialName
				| MethodAttributes.RTSpecialName;
			ConstructorBuilder constructorBuilder = TypeBuilder.DefineConstructor(
				attributes,
				CallingConventions.Standard,
				Type.EmptyTypes);

			ILGeneratorExtender ilGenerator = new ILGeneratorExtender(constructorBuilder.GetILGenerator(), _fieldDictionary);

			return ilGenerator;
		}

		/// <summary>Defines a public constructor.</summary>
		/// <param name="parameterTypeList"></param>
		/// <returns></returns>
		public ILGeneratorExtender DefinePublicInstanceConstructor(params MethodParameter[] parameterList)
		{
			Type[] parameterTypeList = Array.ConvertAll(parameterList, delegate(MethodParameter parameter)
			{
				return parameter.Type;
			});
			ConstructorBuilder constructorBuilder = TypeBuilder.DefineConstructor(
				MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
				CallingConventions.Standard,
				parameterTypeList);

			for (int i = 0; i != parameterList.Length; ++i)
			{
				constructorBuilder.DefineParameter(i + 1, parameterList[i].Attributes, parameterList[i].Name);
			}

			ILGeneratorExtender ilGenerator = new ILGeneratorExtender(constructorBuilder.GetILGenerator(), _fieldDictionary);

			return ilGenerator;
		}

		/// <summary>Defines a private field.</summary>
		/// <param name="fieldName"/>
		/// <param name="fieldType"/>
		/// <param name="isReadOnly"/>
		/// <param name="isStatic"/>
		/// <returns></returns>
		public FieldBuilder DefinePrivateField(string fieldName, Type fieldType, bool isReadOnly, bool isStatic)
		{
			FieldAttributes fieldAttributes = FieldAttributes.Private;

			if (isReadOnly)
			{
				fieldAttributes |= FieldAttributes.InitOnly;
			}
			if (isStatic)
			{
				fieldAttributes |= FieldAttributes.Static;
			}

			FieldBuilder fieldBuilder = TypeBuilder.DefineField(fieldName, fieldType, fieldAttributes);

			_fieldDictionary.Add(fieldName, fieldBuilder);

			return fieldBuilder;
		}
	}
}